package gruppe5.dame.model;

import java.util.ArrayList;
import java.util.List;

import gruppe5.dame.model.types.DameEventType;
import gruppe5.dame.model.types.DameGameType;
import gruppe5.dame.model.types.PawnColor;
import gruppe5.dame.model.types.PawnDirection;

/**
 * Die Spielelogik des Dame Spiels.
 * Erzeugt wird ein Spielbrett mit Spielsteinen, die ueber 
 * das Spielbrett bewegt werden koennen.
 * @author Martin Eutinger
 * @version 1.02
 *
 */
public class DameModel 
{
	

	/************************************************************************************
	 **************************************ELEMENTE**************************************
	 ************************************************************************************/
	
	/**
	 * Die Spielfeldgroesse.
	 * Diese wird einmal im Konstruktor zugewiesen (final).
	 */
	private final int gridSize;
	/**
	 * Die Anzahl der Spielsteine, mit der jeder Spieler startet.
	 * Diese wird einmal im Konstruktor zugewiesen (final).
	 */
	private final int numberOfPawns;
	
	/**
	 * Die Anzahl der bisher initialisierten schwarzen Spielsteine.
	 * Siehe DameModel.initPawns();
	 */
	private int blackPawnCount = 0;
	/**
	 * Die Anzahl der bisher initialisierten weissen Spielsteine.
	 * Siehe DameModel.initPawns();
	 */
	private int whitePawnCount = 0;
	
	/**
	 * Die Liste in der die Spielsteine gespeichert sind.
	 * Es gibt schwarze und weisse Spielsteine.
	 * Weiss beginnt.
	 */
	private List<Pawn> pawns;
	
	/**
	 * Zeigt an welcher Spieler an der Reihe ist.
	 */
	private PawnColor activePlayer = PawnColor.WHITE;
	
	/**
	 * Der Listener der Spielelogik.
	 */
	private DameModelListener modelListener;
	
	
	
	
	/************************************************************************************
	 **********************************INITIALISIERUNG***********************************
	 ************************************************************************************/
	
	/**
	 * Standardkonstruktor fuer die Spielelogik.
	 * Legt die Groesse des Spielfelds und die Anzahl der Spielsteine 
	 * auf 8x8 bzw. 12. Kleines Spielfeld im deutschsprachigen Raum ueblich.
	 * Initialisiert die Liste mit den Spielsteinen.
	 */
	public DameModel()
	{
		this.gridSize = DameGameType.SMALL.GRID_SIZE;
		this.numberOfPawns = DameGameType.SMALL.PAWN_COUNT;
		this.pawns = new ArrayList<Pawn>();
		this.initPawns();
	}
	
	/**
	 * Konstruktor fuer die Spielelogik.
	 * Legt die Groesse des Spielfelds und die Anzahl der Spielsteine 
	 * entsprechend der Angaben in DameGameType fest.
	 * Initialisiert die Liste mit den Spielsteinen.
	 * @param type Spielfeldgroesse und Anzahl der Spielsteine.
	 */
	public DameModel(DameGameType type)
	{
		this.gridSize = type.GRID_SIZE;
		this.numberOfPawns = type.PAWN_COUNT;
		this.pawns = new ArrayList<Pawn>();
		this.initPawns();
	}
	
	/**
	 * Initialisiert die Liste mit den Spielsteinen pawns.
	 * Setzt die Position der Spielsteine und deren Id.
	 * Die zu Beginn verfuegbaren Richtungen in die sich die Spielsteine
	 * bewegen koennen werden ebenfalls gesetzt.
	 * pawns nach der Initialisierung (schwarz oben, weiss unten):
	 * .    1    .    2    .    3    .    4    .    5
	 * 6    .    7    .    8    .    9    .    10   .
	 * .    11   .    12   .    13   .    14   .    15
	 * 16   .    17   .    18   .    19   .    20   .
	 * .    .    .    .    .    .    .    .    .    .
	 * .    .    .    .    .    .    .    .    .    .
	 * .    40   .    39   .    38   .    37   .    36
	 * 35   .    34   .    33   .    32   .    31   .
	 * .    30   .    29   .    28   .    27   .    26
	 * 25   .    24   .    23   .    22   .    21   .
	 */
	private void initPawns()
	{
		int pawnId = 1;
		//schwarze Spielsteine initialisieren
		for(int i = 0; i < this.gridSize; i++)
		{
			for(int j = 0; j < this.gridSize; j++)
			{
				if(((i+j)%2 != 0) && (this.blackPawnCount < this.numberOfPawns))
				{
					this.pawns.add(new Pawn(i, j, PawnColor.BLACK, pawnId));
					this.blackPawnCount++;
					pawnId++;
				}
			}
		}
		
		//weiße Spielsteine initialisieren
		for(int i = (this.gridSize-1); i >= 0; i--)
		{
			for(int j = (this.gridSize-1); j >= 0; j--)
			{
				if(((i+j)%2 != 0) && (this.whitePawnCount < this.numberOfPawns))
				{
					this.pawns.add(new Pawn(i, j, PawnColor.WHITE, pawnId));
					this.whitePawnCount++;
					pawnId++;
				}
			}
		}
		
		this.updateAvailableMoves();
	}
	
	
	
	
	/************************************************************************************
	 ************************************METHODEN****************************************
	 ************************************************************************************/
	
	/************************************************************************************
	 *********************************GETTER/BOOLEANS************************************
	 ************************************************************************************/
	
	/**
	 * Liefert ein Array mit allen Spielsteinen.
	 * @return Ein Array mit allen Spielsteinen.
	 */
	public Pawn[] getAllPawns()
	{
		return this.getPawns().toArray(new Pawn[this.getPawns().size()]);
	}
	
	/**
	 * Liefert ein Array der Spielsteine die sich bewegen muessen.
	 * Wenn ein Spielstein einen anderen schlagen kann muss dies auch geschehen.
	 * Wenn dieses Array keine Spielsteine enthaelt kann ein beliebiger Spielstein
	 * bewegt werden.
	 * @return Eine Liste aller Spielsteine die sich bewegen muessen.
	 */
	public Pawn[] getPawnsThatMustMove()
	{
		return this.getWinningMovePawns().toArray(new Pawn[this.getWinningMovePawns().size()]);
	}
	
	/**
	 * Liefert die Spielfeldgroesse.
	 * @return Die Spielfeldgroesse.
	 */
	public int getGridSize()
	{
		return this.gridSize;
	}
	
	/**
	 * Liefert die Anzahl der Spielsteine, mit der jeder Spieler startet.
	 * F�r die aktuelle Anzahl der Spielsteine siehe getBlackPawnCount()/getWhitePawnCount().
	 * @return Die Anzahl der Spielsteine, mit der jeder Spieler startet.
	 */
	public int getNumberOfPawns()
	{
		return this.numberOfPawns;
	}
	
	/**
	 * Liefert die aktuelle Anzahl der Spielsteine, des schwarzen Spielers.
	 * @return Die aktuelle Anzahl der Spielsteine, des schwarzen Spielers.
	 */
	public int getBlackPawnCount()
	{
		return this.blackPawnCount;
	}
	
	/**
	 * Liefert die aktuelle Anzahl der Spielsteine, des weissen Spielers.
	 * @return Die aktuelle Anzahl der Spielsteine, des weissen Spielers.
	 */
	public int getWhitePawnCount()
	{
		return this.whitePawnCount;
	}
	
	/**
	 * Liefert die Farbe des aktuellen Spielers.
	 * @return Die Farbe des aktuellen Spielers.
	 */
	public PawnColor getCurrentPlayer()
	{
		return this.activePlayer;
	}
	
	/** 
	 * @return Die Liste mit allen Spielsteinen.
	 */
	private List<Pawn> getPawns()
	{
		return this.pawns;
	}
	
	/**
	 * Liefert einen Pawn mit der angegebenen Id.
	 * @param pawnId
	 * @return Der Pawn oder null wenn kein solcher Pawn existiert.
	 */
	private Pawn getPawn(int pawnId)
	{
		for(Pawn pawn : this.getPawns())
		{
			if(pawn.getId() == pawnId)
			{
				return pawn;
			}
		}
		System.err.println("getPawn(): Kein pawn mit id: " +pawnId +" vorhanden");
		return null;
	}
	
	/**
	 * Liefert einen Pawn mit den angegebenen Koordinaten.
	 * @param x
	 * @param y
	 * @return Der Pawn oder null wenn kein solcher Pawn existiert.
	 */
	private Pawn getPawn(int x, int y)
	{
		for(Pawn pawn : this.getPawns())
		{
			if((pawn.getX() == x) && (pawn.getY() == y))
			{
				return pawn;
			}
		}
		System.err.println("getPawn(): Kein pawn mit x: " +x +" und y: " +y +" vorhanden");
		return null;
	}
	
	/**
	 * Liefert ein Array mit den verfuegbaren Bewegungen fuer den Spielstein.
	 * Bewegungen aus Sicht des Standard Spielbretts (schwarz oben, weiss unten).
	 * s.a. DameModel.init().
	 * @param pawnId Der Spielstein fuer den die Bewegungne geliefert werden.
	 * @return Die Liste der verfuegbaren Bewegungen fuer den Spielstein.
	 */
	private List<PawnDirection> calculateAvailableMoves(int pawnId)
	{
		List<PawnDirection> moveList = new ArrayList<PawnDirection>();
		
		//Holt den Spielstein mit pawnId und seine Position
		Pawn tmpPawn = this.getPawn(pawnId);
		if(tmpPawn == null)
			return moveList;
		if(tmpPawn.getColor() != this.activePlayer)
			return moveList;
		
		int tmpX = tmpPawn.getX();
		int tmpY = tmpPawn.getY();
		PawnColor tmpColor = tmpPawn.getColor();
		
		if((tmpPawn.isWhite()) || (tmpPawn.isDame()))
		{
			if(this.isCellEmpty(tmpX-2, tmpY-2) && !this.isCellEmpty(tmpX-1, tmpY-1) && (this.getCellColor(tmpX-1, tmpY-1) != tmpColor))
				moveList.add(PawnDirection.DOUBLE_TOP_LEFT);
			if(this.isCellEmpty(tmpX-2, tmpY+2) && !this.isCellEmpty(tmpX-1, tmpY+1) && (this.getCellColor(tmpX-1, tmpY+1) != tmpColor))
				moveList.add(PawnDirection.DOUBLE_TOP_RIGHT);
			if(this.isCellEmpty(tmpX-1, tmpY-1))
				moveList.add(PawnDirection.TOP_LEFT);
			if(this.isCellEmpty(tmpX-1, tmpY+1))
				moveList.add(PawnDirection.TOP_RIGHT);
		}
		
		if((tmpPawn.isBlack()) || (tmpPawn.isDame()))
		{
			if(this.isCellEmpty(tmpX+2, tmpY+2) && !this.isCellEmpty(tmpX+1, tmpY+1) && (this.getCellColor(tmpX+1, tmpY+1) != tmpColor))
				moveList.add(PawnDirection.DOUBLE_BOTTOM_RIGHT);
			if(this.isCellEmpty(tmpX+2, tmpY-2) && !this.isCellEmpty(tmpX+1, tmpY-1) && (this.getCellColor(tmpX+1, tmpY-1) != tmpColor))
				moveList.add(PawnDirection.DOUBLE_BOTTOM_LEFT);
			if(this.isCellEmpty(tmpX+1, tmpY+1))
				moveList.add(PawnDirection.BOTTOM_RIGHT);
			if(this.isCellEmpty(tmpX+1, tmpY-1))
				moveList.add(PawnDirection.BOTTOM_LEFT);
		}
		
		List<PawnDirection> moveDoubleList = new ArrayList<PawnDirection>();
		boolean doubleFlag = false;
		for(PawnDirection direction : moveList)
		{
			if(direction.toString().contains("DOUBLE"))
			{
				doubleFlag = true;
				moveDoubleList.add(direction);
			}
		}
		
		if(doubleFlag)
			return moveDoubleList;
		else
			return moveList;
	}
	
	/**
	 * Prueft ob ein Spielstein einen anderen Spielstein schlagen kann.
	 * Dann wenn fuer den Spielstein ein Spielzug verfuegbar ist der "DOUBLE"
	 * enthält.
	 * @param pawnId Die Id des zu pruefenden Pawns
	 * @return true wenn der Spielstein nocheinmal ziehen kann.
	 */
	private boolean winningMoveAvailable(int pawnId) 
	{
		for(PawnDirection direction : this.getPawn(pawnId).getAvailableMoves())
		{
			if(direction.toString().contains("DOUBLE"))
				return true;
		}
		return false;
	}
	
	/**
	 * Liefert eine Liste der Spielsteine die sich bewegen muessen.
	 * Wenn ein Spielstein einen anderen schlagen kann muss dies auch geschehen.
	 * Wenn diese Liste keine Spielsteine enthaelt kann ein beliebiger Spielstein
	 * bewegt werden.
	 * @return Eine Liste aller Spielsteine die sich bewegen muessen.
	 */
	private List<Pawn> getWinningMovePawns()
	{
		List<Pawn> pawnList = new ArrayList<Pawn>();
		for(Pawn pawn : this.getPawns())
		{
			if(this.winningMoveAvailable(pawn.getId()) && (pawn.getColor() == this.activePlayer))
			{
				pawnList.add(pawn);
			}
		}
		return pawnList;
	}
	
	/**
	 * Prueft ob sich der Spielstein mit pawnId bewegen muss.
	 * @param pawnId Der zu untersuchende Spielstein.
	 * @return true wenn sich der Spielstein bewegen muss.
	 */
	private boolean pawnMustMove(int pawnId)
	{
		for(Pawn pawn : this.getWinningMovePawns())
		{
			if(pawn.getId() == pawnId)
				return true;
		}
		return false;
	}
	
	/**
	 * Prueft ob sich ein Spielstein in der hintersten Reihe befindet.
	 * Fuer schwarz ist die hinterste Reihe die Reihe auf der zunächst weisse Spieler stehen.
	 * @param pawnId Der zu pruefende Spielstein.
	 * @return true wenn sich der Spielstein in der hintersten Reihe befindet.
	 */
	private boolean isOnBackRank(int pawnId) 
	{
		Pawn pawn = this.getPawn(pawnId);
		if(pawn != null)
		{
			if((pawn.isWhite()) && (pawn.getX() == 0))
				return true;
			if((pawn.isBlack()) && (pawn.getX() == this.gridSize-1))
				return true;
		}
		return false;
	}
	
	/**
	 * Prueft ob sich an der Stelle (x, y) in der Pawn Liste ein Pawn befindet.
	 * @param x X Koordinate der zu pruefenden Zelle.
	 * @param y Y Koordinate der zu pruefenden Zelle.
	 * @return True wenn die Zelle leer ist.
	 */
	private boolean isCellEmpty(int x, int y)
	{
		if((x < 0) || (y < 0) || (x > this.gridSize-1) || (y > this.gridSize-1))
			return false;
		for(Pawn pawn : this.getPawns())
		{
			if((pawn.getX() == x) && (pawn.getY() == y))
				return false;
		}
		return true;
	}
	
	/**
	 * Holt die Farbe des Spielsteins an der Stelle (x, y)
	 * @param x X Koordinate der Zelle des Spielbretts.
	 * @param y Y Koordinate der Zelle des Spielbretts.
	 * @return Die Farbe des Spielsteins an der Stelle, oder null falls kein Spielstein vorhanden.
	 */
	private PawnColor getCellColor(int x, int y) 
	{
		for(Pawn pawn : this.getPawns())
		{
			if((pawn.getX() == x) && (pawn.getY() == y))
			{
				return pawn.getColor();
			}
		}
		return null;
	}
	
	/**
	 * Prueft ob mit dem Spielstein in die angegebene Richtung gezogen werden darf.
	 * @param pawnId Die Id des Pawns der geprueft werden soll.
	 * @param direction Die Richtung die geprueft werden soll.
	 * @return true wenn die Richtung zulässig ist.
	 */
	private boolean isValidDirection(int pawnId, PawnDirection direction)
	{
		for(PawnDirection _direction : this.getPawn(pawnId).getAvailableMoves())
		{
			if(_direction == direction)
				return true;
		}
		return false;
	}
		
	/**
	 * Prueft ob sich der Spieler activePlayer noch bewegen kann.
	 * @param _activePlayer Der Spieler dessen Bewegungsmoeglichkeiten geprueft werden.
	 * @return true wenn sich der Spieler noch bewegen kann.
	 */
	private boolean isPlayerBlocked(PawnColor _activePlayer) 
	{
		for(Pawn pawn : this.getPawns())
		{
			if(pawn.getAvailableMoves().size() != 0)
				return false;
		}
		return true;
	}

	
	
	
	
	/************************************************************************************
	 *********************************SPIELSTEINE****************************************
	 ************************************************************************************/
	
	/**
	 * Bewegt den Spielstein mit der pawnId in die angegebene Richtung.
	 * @param pawnId Der zu bewegende Spielstein.
	 * @param direction Die Richtung in die der Pawn bewegt werden soll. 
	 */
	public void movePawn(int pawnId, PawnDirection direction)
	{
		Pawn pawn = this.getPawn(pawnId);
		
		/***************************PRUEFEN DER GUELTIGKEIT*******************************/
		if(pawn == null)
		{
			System.err.println("movePawn(): keinen Pawn erhalten");
			return;
		}
		
		//Prueft ob der Spieler, dessen Spielstein bewegt werden soll, an der Reihe ist
		if(pawn.getColor() != this.activePlayer)
		{
			System.err.println("movePawn(): Spieler " +this.activePlayer +" ist am Zug");
			return;
		}
		
		//Prueft ob es Steine gibt die sich bewegen muessen bevor sich dieser Stein bewegt
		if(this.getWinningMovePawns().size() != 0)
		{
			if(!this.pawnMustMove(pawnId))
			{
				System.err.println("movePawn(): Ein anderer Spielstein muss bewegt werden");
				return;
			}
		}
		
		//Prueft ob der Spielstein in eine zulaessige Richtung bewegt werden soll
		if(!this.isValidDirection(pawnId, direction))
		{
			System.err.println("movePawn(): Unzulässige Zugrichtung");
			return;
		}
		
		/***************************PRUEFEN DER GUELTIGKEIT ENDE*************************/
		
		
		/*******************************SPIELSTEIN BEWEGEN*****************************/
		int oldX = pawn.getX();
		int oldY = pawn.getY();
		
		pawn.move(direction);

		switch(direction)
		{
		case DOUBLE_TOP_LEFT:
			this.removePawn(oldX-1, oldY-1);
			break;
		case DOUBLE_TOP_RIGHT:
			this.removePawn(oldX-1, oldY+1);
			break;
		case DOUBLE_BOTTOM_LEFT:
			this.removePawn(oldX+1, oldY-1);
			break;
		case DOUBLE_BOTTOM_RIGHT:
			this.removePawn(oldX+1, oldY+1);
			break;
		default:
			break;
		}	
		
		this.updateAvailableMoves();
		
		/*******************************SPIELSTEIN BEWEGEN ENDE************************/

		/***********************************ADDITIONAL*********************************/
		//pruefen ob ein Spieler alle Spielstein verloren hat
		if(this.blackPawnCount == 0) {
			this.throwModelChangedEvent(DameEventType.WHITE_WON);
			return;
		} else if(this.whitePawnCount == 0) {
			this.throwModelChangedEvent(DameEventType.BLACK_WON);
			return;
		}
		
		//pruefen ob ein Spielstein zur Dame befoerdert werden muss
		if(this.isOnBackRank(pawn.getId()))
		{
			if(!pawn.isDame())
				this.promotePawn(pawn.getId());
		}
		
		//pruefen ob Spieler ein weiteres mal ziehen darf
		if(this.winningMoveAvailable(pawn.getId()) && direction.toString().contains("DOUBLE"))
		{
			if(this.activePlayer == PawnColor.WHITE)
				this.throwModelChangedEvent(DameEventType.WHITE_MOVES_AGAIN);
			else
				this.throwModelChangedEvent(DameEventType.BLACK_MOVES_AGAIN);
		}else{
			this.toggleActivePlayer();
			if(this.activePlayer == PawnColor.WHITE)
				this.throwModelChangedEvent(DameEventType.WHITE_MOVES);
			else
				this.throwModelChangedEvent(DameEventType.BLACK_MOVES);
		}
		
		//pruefen ob Spieler blockiert ist
		if(this.isPlayerBlocked(this.activePlayer))
		{
			if(this.activePlayer == PawnColor.BLACK)
			{
				this.throwModelChangedEvent(DameEventType.WHITE_WON);
			} else if(this.activePlayer == PawnColor.WHITE){
				this.throwModelChangedEvent(DameEventType.BLACK_WON);
			}
			System.err.println(activePlayer +" Player kann sich nicht mehr bewegen. Spiel beendet");
			return;
		}
		
		this.updateAvailableMoves();
		/***********************************ADDITIONAL ENDE****************************/
	}
	
	/**
	 * Entfernt den Pawn an der Stelle (x,y) von der Pawn Liste.
	 * @param x X Koordinate des zu entfernenden Pawns.
	 * @param y Y Koordinate des zu entfernenden Pawns.
	 */
	private void removePawn(int x, int y)
	{
		Pawn pawn = this.getPawn(x, y);
		if(pawn == null)
			return;
		
		if(pawn.isBlack())
			this.blackPawnCount--;
		else
			this.whitePawnCount--;
		this.getPawns().remove(pawn);
	}
	
	/**
	 * Macht einen normalen Spielstein zur Dame.
	 * @param pawnId
	 */
	private void promotePawn(int pawnId) 
	{
		Pawn pawn = this.getPawn(pawnId);
		if(!pawn.isDame())
			pawn.promote();
		else
			System.err.println("promotePawn(): Spielstein ist bereits eine Dame.");
	}
	
	
	
	
	
	/************************************************************************************
	 ************************************MISC********************************************
	 ************************************************************************************/
	
	/**
	 * Updatet die Liste der verfuegbaren Richtungen eines jeden Spielsteins.
	 */
	private void updateAvailableMoves()
	{
		boolean doubleFlag = false;
		for(Pawn pawn : this.getPawns())
		{
			pawn.setAvailableMoves(this.calculateAvailableMoves(pawn.getId()));
			for(PawnDirection direction : pawn.getAvailableMoves())
			{
				if(direction.toString().contains("DOUBLE"))
				{
					doubleFlag = true;
					break;
				}
			}
		}
		if(!doubleFlag)
			return;
		
		for(Pawn pawn : this.getPawns())
		{
			doubleFlag = false;
			for(PawnDirection direction : pawn.getAvailableMoves())
			{
				if(direction.toString().contains("DOUBLE"))
				{
					doubleFlag = true;
					break;
				}
			}
			if(!doubleFlag)
				pawn.clearAvailableMoves();
		}
		return;
	}
	
	/**
	 * Aendert welcher Spieler an der Reihe ist.
	 */
	private void toggleActivePlayer() 
	{
		if(this.activePlayer == PawnColor.WHITE)
			this.activePlayer = PawnColor.BLACK;
		else
			this.activePlayer = PawnColor.WHITE;
	}
	
	/**
	 * Registriert einen DameModelListener bei der Spielelogik.
	 * @param _modelListener Der DameModelListener der registriert werden soll.
	 */
	public void addDameModelListener(DameModelListener _modelListener)
	{
		if(this.modelListener == null)
		{
			this.modelListener = _modelListener;
		}else{
			System.err.println("addDameModelListener(): Listener darf nur einmal registriert werden.");
		}
	}
	
	/**
	 * Erzeugt ein Event und ruft die Funktion des DameModelListeners auf.
	 */
	private void throwModelChangedEvent(DameEventType type)
	{
		if(this.modelListener == null)
		{
			System.err.println("throwModelChangedEvent(): Listener wurde noch nicht initialisiert.");
			return;
		}
		
		this.modelListener.dameChanged(new DameModelChangedEvent(type));
	}

}
